[日本語Alexa] 社員証をかざして出退勤を記録するスキルを作ってみた
1 はじめに
今回は、カードリーダーとAlexaの連携サンプルとして、出退勤を記録するスキルを作ってみました。
社員証を使用することで、誰がAlexaに話しかけたのかを判別しています。
最初に、動作している様子をご覧ください。
社員証をかざすと出退勤が記録できます。社員証をかざさなかったり、社員証以外のカード(Felica)を提示すると怒られます。
2 構成
構成は、図のとおりです。
カードリーダーに社員証をかざすと、IDを読み取ってMQTTでパブリッシュします。AWS IoT Coreでは、ルールに基づいてその内容をDynamoDB(カード情報保存DB)に書き込みます。
Echoから呼び出されたスキルは、最初にカード情報保存DBを確認し、10秒以内に更新された情報を対象に、カードのIDが有効なものかを確認してから動作を初めます。
スキルは、社員証が有効な場合のみ、勤怠記録DBに出勤若しくは、退勤を記録します。
勤怠記録DBが更新されると、Webページ更新用のLambdaが起動され、表示用のWebページを更新してS3に格納します。また、同時にMQTTで、ブラウザにページが更新されたことを伝えます。
ブラウザは、サブスクライブ状態で待機しており、トピックが来ると、ページをリフレッシュしています。
3 カードリーダー
今回使用したカードリーダーは、ソニー SONY 非接触ICカードリーダー/ライター PaSoRi RC-S380です。
PythonでNFCリーダを取り扱うライブラリとしてnfcpyを利用させて頂きました。
$ sudo pip install nfcpy $ git clone https://github.com/nfcpy/nfcpy.git
サンプルコードで、動作の確認を行うことができます。
$ sudo python nfcpy/examples/tagtool.py No handlers could be found for logger "nfc.llcp.sec" [nfc.clf] searching for reader on path usb [nfc.clf] using SONY RC-S380/P NFC Port-100 v1.11 at usb:001:004 ** waiting for a tag **
試しにKitaca(Suicaの北海道版)を読ませてみると下記のように表示されてました。
Type3Tag 'FeliCa Standard (RC-S???)' ID=010102XXXXXXX0C25 PMM=100B4XXXXXXXD0FF SYS=0003
4 AWS IoT Core
(1) Publish
作成した「モノ」は、attendanceです。証明書を発行し、デバイスにコピーします。
パブリッシュが正常に動作しているかどうかは、AWSコンソールのテストで確認できます。
(2) ルール
続いて、ルールを設定して、publishされたデータをDynamoDBに書き込みます。
テーブルは、予め作成しておきます。
そして、AWSコンソールのACTからルールを追加します。
DynamoDBへのアクションの設定は、以下のようになっています。
正常に動作するとDynamoDBのデータが更新されることを確認できます。
5 コード(RaspberryPi上)
デバイス側でカードをIDを読み取って、パブリッシュするコードは、以下のとおりです。
コマンドラインからは、以下のように使用します。(sudoしているのは、カードリーダーデバイスの使用のための権限です)
$sudo node ./card.js Device001
card.js
<br />const deviceModule = require('./node_modules/aws-iot-device-sdk').device; const exec = require('child_process').exec; const topicName = 'attendance_card'; const host = 'xxxxxxxxx.iot.us-east-1.amazonaws.com' const region = 'us-east-1'; const clientId = 'attendance'; const device_id = process.argv[2]; const device = deviceModule({ keyPath: './cert/private.key', certPath: './cert/cert.pem', caPath: './cert/root-CA.crt', clientId: clientId, host: host, region: region, reconnectPeriod:10 }); device.on('connect', async () => { // MQTT接続完了 console.log('device connect'); while(true){ try { // カード情報の読み取り const card_id = await card_read(); let data = { date_time: create_datetime_string(), device_id: device_id, card_id : card_id, }; // パブリッシュ device.publish(topicName, JSON.stringify(data)); console.log("publish " + JSON.stringify(data)); // チャイム await chime(); } catch(error) { console.log(error); } } }); function create_datetime_string() { var now = new Date(); return now.getFullYear() + '/' + ("0" + ( now.getMonth() + 1 ) ).slice(-2) + '/' + ("0" + now.getDate() ).slice(-2) + ' ' + ("0" + now.getHours()).slice(-2) + ':' + ("0" + now.getMinutes()).slice(-2) + ':' + ("0" + now.getSeconds()).slice(-2); } async function card_read(){ return new Promise((resolve, reject) => { exec('python ../nfc/nfcpy/examples/tagtool.py', (err, stdout, stderr) => { if (err) { reject(err); } else { const card_id = stdout.match(/ID=(.*?)\s/); resolve(card_id[1]); } }); }) } async function chime(){ return new Promise((resolve, reject) => { const cmd = 'mpg321 chime.mp3'; exec(cmd, (err, stdout, stderr) => { if (err) { reject(err); } else { console.log(cmd); resolve("success"); } }); }) }
6 スキル作成
(1) インテント
インテントとして定義したのは、以下のようなものです。
- RecordIntent 記録して/タイムカード
- WorkingHoursOfDayIntent 今日の勤務時間は?/今日は何時間だった?
- WorkingHoursOfMonthIntent 先月の勤務時間は?/先月は何時間だった?
勤怠の記録を行っているメインのインテントはRecordIntentです。
const RecordIntentHandler = { canHandle(h) { return isMatch(h, 'RecordIntent', 'LaunchRequest'); }, async handle(h) { console.log(JSON.stringify(h.requestEnvelope)); // DynamoDBからカードIDを取得する const card_id = await cardRead('Device001'); // カードIDから社員の名前を取得する const name = getNameFromCardId(card_id); let speak = ''; if(card_id == undefined){ speak = '社員証をかざしてからご利用ください'; } else if (name == undefined) { speak = 'IDが無効です。この社員証はご利用になれません。'; } else { // 日付文字列の生成 const datetime = CreateDateTime(); const time = CreateTime(); // 勤怠記録DBに記録する(出勤を記録した場合1、退勤を記録した場合0が返される) const state = await record(datetime, card_id); if(state == 1) { speak = name + 'さん、おはようございます。' + time + '<break time="300ms"/>出勤を記録しました。今日もいちにち、宜しくお願いいたします。' } else { speak = name + 'さん、お疲れ様でした。 <break time="1s"/>' + time + '<break time="300ms"/>退勤を記録しました。' } } return h.responseBuilder .speak(speak) .getResponse(); } };
7 勤怠記録DBの更新とWebページの更新
スキルで社員証が認識され、勤怠記録が走ると、出勤若しくは、退勤の時間がDBに保存されます。
このDBが更新されたタイミングで発火されるLambdaは、下記のとおりです。
予めブラウザ表示用のページの元となるファイル(base.html)をS3に配置しておき、そのファイルを読み込んで、データベースの内容を反映したindex.htmlを更新します。また、同時に、ブラウザに対してMQTTで更新されたことを伝え、リフレッシュさせています。
const AWS = require("./AWS"); const tableName = 'attendance_record_table'; const bucket = 'attendance-html'; const srcHtml = 'base.html'; const dstHtml = 'index.html'; const endpoint = 'xxxxxxxxxx.iot.us-east-1.amazonaws.com'; const topic = "attendance_refresh"; exports.handler = async (event) => { const aws = new AWS(); // S3からbase.htmlをダウンロードする const srcData = await aws.s3_get(bucket, srcHtml); let text = decodeURIComponent(escape(String.fromCharCode.apply(null, srcData.Body))); // DynamoDBの勤怠レコードでbase.htmlからindex.htmlを作成する const scanData = await aws.dynamoDb_scan(tableName); if (scanData) { // 日付でソート scanData.Items.sort(function(a,b){ if( a.datetime.S < b.datetime.S ) return 1; if( a.datetime.S > b.datetime.S ) return -1; return 0; }); let n = 0; scanData.Items.forEach ( item => { if(n < 10){ text = text.replace('${DateTime_0'+ n + '}',item.datetime.S); text = text.replace('${Name_0'+ n + '}',createNameString(item.card_id.S)); text = text.replace('${State_0'+ n + '}',createStateString(item.state.N)); n++; } }); } // index.htmlをアップロードする const contentType = "text/html"; await aws.s3_upload(bucket, dstHtml, text, contentType); // MQTTでブラウザに更新されたことを伝える let result = await aws.iot_publish(topic, endpoint, '{"action":"refresh"}'); console.log(result); };
予め用意されているファイル(base.html)は、以下のようなものです。
そして更新されたファイル(index.html)は、次のようになります。
8 最後に
今回は、カードリーダーとAlexaの連携を試してみました。 AWS Iot を経由することで、AWSのリソースと簡単に連接できました。
なんでも自由につなげそうですが、あくまで、起動がAlexa側になるので、そのUIをどのように設計するかは、非常に大事だと感じました。
カードをかざしたときの各種の効果音は、効果音ラボのものを利用させて頂きました。